Skip to content

fix: add Next.js RSC headers to CloudFront CachePolicy to prevent cache poisoning#118

Closed
konokenj wants to merge 2 commits intomainfrom
fix/cloudfront-rsc-cache-headers
Closed

fix: add Next.js RSC headers to CloudFront CachePolicy to prevent cache poisoning#118
konokenj wants to merge 2 commits intomainfrom
fix/cloudfront-rsc-cache-headers

Conversation

@konokenj
Copy link
Copy Markdown
Contributor

Summary

Add Next.js App Router RSC headers (RSC, Next-Router-Prefetch, Next-Router-State-Tree, Next-URL) to the CloudFront CachePolicy CacheHeaderBehavior.allowList to prevent cache poisoning between HTML and RSC flight responses.

Closes #100

Problem

Next.js App Router sends two types of requests to the same URL:

  1. HTML requests — full page loads
  2. RSC requests — client-side navigation with RSC: 1 header

Without these headers in the cache key, CloudFront treats both request types identically. When caching is active (static pages, ISR, or explicit cache headers), an RSC response (text/x-component) can be cached and served for a normal HTML request, or vice versa.

The current sample app uses dynamic rendering (cookies() calls), so Cache-Control: private, no-cache, no-store prevents caching. However, as a starter kit, users will naturally add static pages or ISR, at which point cache poisoning occurs.

Change

 headerBehavior: CacheHeaderBehavior.allowList(
   'authorization',
   'Origin',
   'X-HTTP-Method-Override',
   'X-HTTP-Method',
   'X-Method-Override',
+  'RSC',
+  'Next-Router-Prefetch',
+  'Next-Router-State-Tree',
+  'Next-URL',
 ),

Grounding / References

The fix is validated by multiple independent sources:

  • CVE-2025-49005 (Vercel changelog, GHSA-r2fc-ccr8-96c4): Next.js 15.3.0–15.3.3 had a cache poisoning vulnerability due to missing Vary header. Vercel's recommended workaround for self-hosted deployments: manually set Vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch.
  • Running Next.js behind AWS CloudFront: Documents the exact same issue — RSC responses cached and served as HTML — and recommends adding these headers to CloudFront Cache Policy.
  • cdk-nextjs (jetbridge/cdk-nextjs): Uses rsc, next-router-prefetch, next-router-state-tree, next-url as cache key components (hashed via CloudFront Function into x-open-next-cache-key).
  • CloudFront quota: Cache Policy allows up to 10 headers. Current 5 + 4 added = 9, within the limit.

Verification

  • CDK build passes (npm run build in cdk/)
  • After deployment, HTML requests and RSC requests (RSC: 1 header) to the same URL should produce separate cache entries

…he poisoning

Add RSC, Next-Router-Prefetch, Next-Router-State-Tree, and Next-URL
headers to CacheHeaderBehavior.allowList so that CloudFront
distinguishes RSC flight responses from HTML responses in the cache key.

Without these headers, static/ISR pages can serve text/x-component
data for normal HTML requests (or vice versa) when CloudFront caching
is active.

Closes #100
@konokenj konokenj added this to the v2-fix milestone Mar 20, 2026
@konokenj
Copy link
Copy Markdown
Contributor Author

Closing in favor of a revised approach using CloudFront Functions to hash RSC headers into a single cache key header, avoiding the 10-header limit on CloudFront Cache Policies.

@konokenj konokenj closed this Mar 20, 2026
@konokenj konokenj deleted the fix/cloudfront-rsc-cache-headers branch March 20, 2026 07:24
konokenj added a commit that referenced this pull request Mar 22, 2026
## Summary

Prevent CloudFront cache poisoning between HTML and RSC flight responses
by adding a CloudFront Function that hashes Next.js RSC headers into a
single cache key header.

Closes #100
Supersedes #118

## Problem

Next.js App Router sends two types of requests to the same URL:
1. **HTML requests** — full page loads
2. **RSC requests** — client-side navigation with `RSC: 1` header,
returning `text/x-component` flight data

Next.js sets `Vary: rsc, next-router-state-tree, next-router-prefetch,
next-router-segment-prefetch` (plus `next-url` for interception routes)
to signal that responses differ based on these headers. However,
CloudFront does not honor `Vary` — headers must be explicitly included
in the Cache Policy to become part of the cache key.

Without this fix, when CloudFront caching is active (static pages, ISR,
or explicit cache headers), an RSC response can be cached and served for
a normal HTML request, or vice versa.

## Approach

Adding all 5 RSC headers directly to the Cache Policy would hit
CloudFront's **10-header limit** (5 existing + 5 = 10, no room for
future additions). Instead, we use a **CloudFront Function**
(VIEWER_REQUEST) that:

1. Reads the 5 Next.js RSC headers (`rsc`, `next-router-prefetch`,
`next-router-state-tree`, `next-router-segment-prefetch`, `next-url`)
2. Hashes them into a single `x-nextjs-cache-key` header using FNV-1a
3. The Cache Policy includes only `x-nextjs-cache-key` (6/10 headers
used)

This is the same approach used by
[cdk-nextjs](https://github.com/jetbridge/cdk-nextjs) (which hashes into
`x-open-next-cache-key`).

### Why CloudFront Function (not Lambda@Edge)?

The existing `sign-payload` Lambda@Edge (ORIGIN_REQUEST) handles request
body hashing for SigV4, which requires body access — only possible with
Lambda@Edge. The RSC header hashing is a lightweight header-only
operation ideal for CloudFront Functions. Both coexist on the same
behavior (CF Function at VIEWER_REQUEST, L@E at ORIGIN_REQUEST).

## Files changed

- `cdk/lib/constructs/cf-lambda-furl-service/cf-function/cache-key.js` —
New CloudFront Function
- `cdk/lib/constructs/cf-lambda-furl-service/service.ts` — Wire up CF
Function + add `x-nextjs-cache-key` to Cache Policy

## Grounding / References

- **Next.js source (v16.1.6)** `base-server.js:setVaryHeader()` —
Confirms `Vary` includes `rsc`, `next-router-state-tree`,
`next-router-prefetch`, `next-router-segment-prefetch` for all App
Router pages, plus `next-url` for interception routes
- **Next.js source** `app-render.js:149` —
`next-router-segment-prefetch: /_tree` triggers a different response
(route tree only), confirming it must be in the cache key
- **CVE-2025-49005**
([Vercel](https://vercel.com/changelog/cve-2025-49005),
[GHSA-r2fc-ccr8-96c4](GHSA-r2fc-ccr8-96c4))
— Cache poisoning via missing `Vary` header in Next.js 15.3.0–15.3.3.
Workaround: manually set `Vary: RSC, Next-Router-State-Tree,
Next-Router-Prefetch`
- **[Running Next.js behind AWS
CloudFront](https://www.bstefanski.com/blog/running-nextjs-behind-aws-cloudfront)**
— Documents the same cache poisoning issue and fix
- **[cdk-nextjs](https://github.com/jetbridge/cdk-nextjs)**
`NextjsDistribution.ts` — Uses the same hash-into-single-header approach
with `x-open-next-cache-key`
- **[CloudFront
quotas](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html)**
— Cache Policy allows max 10 headers
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CloudFront CachePolicyにNext.js RSCヘッダーが含まれておらずキャッシュ汚染が発生する

1 participant